import hashlib
import json
import os
import sys
import time

import helpr

os.chdir("/www/server/panel")
sys.path.append("class/")
import public, pwd


class panel_api:
    __BT_KEY = None
    __BT_PANEL = None
    _REQUESTS = None
    _PLUGIN_PATH = "/www/server/panel/plugin/remoteback"
    _SPEED_FILE = _PLUGIN_PATH + '/config/speed.json'
    _buff_size = 1024 * 1024 * 2
    _API_INFO = None

    def __init__(self, api_info=None):
        """
        如果希望多台面板, 可以在实例化对象时, 将面板地址与密钥传入
        :param api_info:
        """
        if api_info:
            self.API_INFO = api_info
            self.__BT_PANEL = api_info['panel']
            self.__BT_KEY = api_info['token']
            self._SPEED_FILE = self._PLUGIN_PATH + f"/config/speed_{api_info['config_name']}.json"
        import requests
        if not self._REQUESTS:
            self._REQUESTS = requests.session()

    def __get_md5(self, s):
        """
        计算MD5
        :param s:
        :return:
        """
        m = hashlib.md5()
        m.update(s.encode('utf-8'))
        return m.hexdigest()

    def __get_key_data(self):
        """
        构造带有签名的关联数组
        :return:
        """
        now_time = int(time.time())
        p_data = {
            'request_token': self.__get_md5(str(now_time) + '' + self.__get_md5(self.__BT_KEY)),
            'request_time': now_time
        }
        return p_data

    def __http_post_cookie(self, url, p_data, timeout=1800):
        """
        发送POST请求并保存Cookie
        :param url: 被请求的URL地址(必需)
        :param p_data: POST参数, 可以是字符串或字典(必需)
        :param timeout: 超时时间默认1800秒
        :return: string
        """
        try:
            res = self._REQUESTS.post(url, p_data, timeout=timeout, verify=False)
            return res.text
        except Exception as ex:
            ex = str(ex)
            if ex.find('Max retries exceeded with') != -1:
                return public.returnJson(False, '连接服务器失败!' + ex)
            if ex.find('Read timed out') != -1 or ex.find('Connection aborted') != -1:
                return public.returnJson(False, '连接超时!')
            return public.returnJson(False, '连接服务器失败!' + ex)

    def upload_file(self, sfile, dfile, chmod=None):
        """
        上传文件
        :param sfile: 上传到的文件路径以及文件名
        :param dfile: 要上传的文件
        :param chmod: 文件权限
        :return:
        """
        if not os.path.exists(sfile):
            helpr.write_log("|-指定目录不存在{}".format(sfile))
            return False
        pdata = self.__get_key_data()
        pdata['f_name'] = os.path.basename(dfile)
        pdata['f_path'] = os.path.dirname(dfile)
        pdata['f_size'] = os.path.getsize(sfile)
        pdata['f_start'] = 0
        if chmod:
            mode_user = self.get_mode_and_user(os.path.dirname(sfile))
            pdata['dir_mode'] = mode_user['mode'] + ',' + mode_user['user']
            mode_user = self.get_mode_and_user(sfile)
            pdata['file_mode'] = mode_user['mode'] + ',' + mode_user['user']
        f = open(sfile, 'rb')
        return self.send_file(pdata, f)

    def send_file(self, pdata, f):
        """
        发送文件
        :param pdata: 文件数据
        :param f: 文件对象
        :return:
        """
        success_num = 0  # 连续发送成功次数
        max_buff_size = int(1024 * 1024 * 2)  # 最大分片大小
        min_buff_size = int(1024 * 32)  # 最小分片大小
        err_num = 0  # 连接错误计数
        max_err_num = 10  # 最大连接错误重试次数
        up_buff_num = 5  # 调整分片的触发次数
        timeout = 60  # 每次发送分片的超时时间
        split_num = 0
        split_done = 0
        total_time = 0
        self.write_speed('done', "正在传输文件")
        self.write_speed('size', pdata['f_size'])
        self.write_speed('used', 0)
        self.write_speed('speed', 0)
        helpr.write_log(
            "|-上传文件[{}], 总大小：{}, 当前分片大小为：{}".format(pdata['f_name'], helpr.to_size(pdata['f_size']),
                                                                  helpr.to_size(self._buff_size)))
        while True:
            buff_size = self._buff_size
            max_buff = int(pdata['f_size'] - pdata['f_start'])
            if max_buff < buff_size: buff_size = max_buff  # 判断是否到文件尾
            files = {"blob": f.read(buff_size)}
            start_time = time.time()
            try:
                res = self._REQUESTS.post(self.__BT_PANEL + '/files?action=upload', data=pdata, files=files,
                                          timeout=timeout)
                success_num += 1
                err_num = 0
                # 连续5次分片发送成功的情况下尝试调整分片大小, 以提升上传效率
                if success_num > up_buff_num and self._buff_size < max_buff_size:
                    self._buff_size = int(self._buff_size * 2)
                    success_num = up_buff_num - 3  # 如再顺利发送3次则继续提升分片大小
                    if self._buff_size > max_buff_size: self._buff_size = max_buff_size
                    helpr.write_log("|-发送顺利, 尝试调整分片大小为: {}".format(helpr.to_size(self._buff_size)))
            except Exception as ex:
                times = time.time() - start_time
                total_time += times
                ex = str(ex)
                if ex.find('Read timed out') != -1 or ex.find('Connection aborted') != -1:
                    # 发生超时的时候尝试调整分片大小, 以确保网络情况不好的时候能继续上传
                    self._buff_size = int(self._buff_size / 2)
                    if self._buff_size < min_buff_size: self._buff_size = min_buff_size
                    success_num = 0
                    helpr.write_log("|-发送超时, 尝试调整分片大小为: {}".format(helpr.to_size(self._buff_size)))
                    continue

                # 如果连接超时
                if ex.find('Max retries exceeded with') != -1 and err_num <= max_err_num:
                    err_num += 1
                    helpr.write_log("|-连接超时, 第{}次重试".format(err_num))
                    time.sleep(1)
                    continue

                # 超过重试次数
                helpr.write_log("|-上传失败, 跳过本次上传任务")
                helpr.write_log(public.get_error_info())
                return False

            result = res.json()
            times = time.time() - start_time
            total_time += times
            if type(result) == int:
                if result == split_done:
                    split_num += 1
                else:
                    split_num = 0
                split_done = result
                if split_num > 10:
                    helpr.write_log("|-上传失败, 跳过本次上传任务")
                    return False
                if result > pdata['f_size']:
                    helpr.write_log("|-上传失败, 跳过本次上传任务")
                    return False
                self.write_speed('used', result)
                self.write_speed('speed', int(buff_size / times))
                helpr.write_log(
                    "|-已上传 {},上传速度 {}/s, 共用时 {}分{:.2f}秒,  {:.2f}%".format(helpr.to_size(float(result)),
                                                                                      helpr.to_size(buff_size / times),
                                                                                      int(total_time // 60),
                                                                                      total_time % 60, (float(
                            result) / float(pdata['f_size']) * 100)))
                pdata['f_start'] = result  # 设置断点
            else:
                if not result['status']:  # 如果服务器响应上传失败
                    helpr.write_log(result['msg'])
                    return False

                if pdata['f_size']:
                    self.write_speed('used', pdata['f_size'])
                    self.write_speed('speed', int(buff_size / times))
                    helpr.write_log("|-已上传 {},上传速度 {}/s, 共用时 {}分{:.2f}秒,  {:.2f}%".format(
                        helpr.to_size(float(pdata['f_size'])), helpr.to_size(buff_size / times), int(total_time // 60),
                        total_time % 60, (float(pdata['f_size']) / float(pdata['f_size']) * 100)))
                break
        self.write_speed('total_size', pdata['f_size'])
        self.write_speed('end_time', int(time.time()))
        helpr.write_log("|-总耗时：{} 分钟, {:.2f} 秒, 平均速度：{}/s".format(int(total_time // 60), total_time % 60,
                                                                            helpr.to_size(
                                                                                pdata['f_size'] / total_time)))
        return True

    def write_speed(self, key, value):
        """
        写文件发送进度
        :param key:
        :param value:
        :return:
        """
        if os.path.exists(self._SPEED_FILE):
            speed_info = json.loads(public.readFile(self._SPEED_FILE))
        else:
            speed_info = {"time": int(time.time()), "size": 0, "used": 0, "total_size": 0, "speed": 0,
                          "action": "等待中", "done": "等待中", "end_time": int(time.time())}
        if not key in speed_info: speed_info[key] = 0
        if key == 'total_size':
            speed_info[key] += value
        else:
            speed_info[key] = value
        public.writeFile(self._SPEED_FILE, json.dumps(speed_info))

    def send_panel(self, uri, data, timeout=360):
        """
        发送请求到面板
        :param uri: url路径
        :param data: 发送数据
        :param timeout: 超时，默认360秒
        :return:
        """
        pdata = self.__get_key_data()
        data['request_token'] = pdata['request_token']
        data['request_time'] = pdata['request_time']
        # 确保 URL 拼接正确，避免双斜杠
        panel_url = self.__BT_PANEL.rstrip('/')
        uri = uri.lstrip('/')
        result = self.__http_post_cookie(panel_url + '/' + uri, data, timeout)
        try:
            result = json.loads(result)
            if type(result) not in [dict, list]:
                helpr.write_log_error('面板返回数据格式错误，原始数据: ' + str(result))
                return result
            return result
        except:
            return result

    def get_mode_and_user(self, path):
        """
        取文件或目录权限信息
        :param path:文件路径
        """
        data = {}
        if not os.path.exists(path): return None
        stat = os.stat(path)
        data['mode'] = str(oct(stat.st_mode)[-3:])
        try:
            data['user'] = pwd.getpwuid(stat.st_uid).pw_name
        except:
            data['user'] = str(stat.st_uid)
        return data

    def check_site_exist(self, site_name):
        """
        检查远程站点是否存在
        :param site_name: 站点名称
        :return:
        """
        ret = self.send_panel(
            'datalist/data/get_data_list',
            {'type': -1, 'search': site_name, 'limit': 10, 'table': 'sites', 'order': ''}
        )
        if not type(ret) == dict: return False
        if 'data' not in ret: return False
        if not ret['data']: return False
        if len(ret['data']) > 0:
            for data in ret['data']:
                if data['name'] == site_name:
                    return True
        return False

    def check_db_exist(self, db_name):
        """
        检查远程数据库是否存在
        :param db_name: 数据库名称
        :return:
        """
        ret = self.send_panel(
            'datalist/data/get_data_list',
            {'type': -1, 'search': db_name, 'limit': 10, 'table': 'databases', 'order': ''}
        )
        if not type(ret) == dict: return False
        if 'data' not in ret: return False
        if not ret['data']: return False
        if len(ret['data']) > 0:
            return True
        return False

    def get_dst_config(self):
        """
        获取目标服务器和环境配置
        :return:
        """
        ret = self.send_panel('system?action=GetConcifInfo', {})
        disk = self.send_panel('system?action=GetDiskInfo', {})
        if 'webserver' in ret:
            result = {}
            result['php'] = []
            if 'webserver' in ret:
                result['webserver'] = ret['webserver']
            if 'mysql' in ret:
                result['mysql'] = ret['mysql']['status']
            if 'pure-ftpd' in ret:
                result['ftp'] = ret['pure-ftpd']['status']
            if 'php' in ret:
                for i in ret['php']:
                    result['php'].append(i['version'])
            result['status'] = True
            result['version'] = 6
            result['disk'] = disk
            return result
        else:
            return False

    def get_src_config(self):
        """
        获取本地服务器和环境配置
        :return:
        """
        serverInfo = {}
        serverInfo['status'] = True
        serverInfo['webserver'] = '未安装'
        if os.path.exists('/www/server/nginx/sbin/nginx'): serverInfo['webserver'] = 'nginx'
        if os.path.exists('/www/server/apache/bin/httpd'): serverInfo['webserver'] = 'apache'
        if os.path.exists('/usr/local/lsws/bin/lswsctrl'): serverInfo['webserver'] = 'openlitespeed'
        serverInfo['php'] = []
        phpversions = ['52', '53', '54', '55', '56', '70', '71', '72', '73', '74', '80', '81', '82', '83', '84']
        phpPath = '/www/server/php/'
        for pv in phpversions:
            if not os.path.exists(phpPath + pv + '/bin/php'): continue
            serverInfo['php'].append(pv)
        serverInfo['mysql'] = False
        if os.path.exists('/www/server/mysql/bin/mysql'): serverInfo['mysql'] = True
        serverInfo['ftp'] = False
        if os.path.exists('/www/server/pure-ftpd/bin/pure-pw'): serverInfo['ftp'] = True
        if os.path.exists('/www/server/panel/runserver.py'):
            serverInfo['version'] = 6
        else:
            serverInfo['version'] = 5
        import psutil
        try:
            diskInfo = psutil.disk_usage('/www')
        except:
            diskInfo = psutil.disk_usage('/')
        serverInfo['disk'] = diskInfo[2]
        return serverInfo

    def get_dst_task_speed(self):
        """
        获取远程服务器执行中的任务列表
        :return:
        """
        ret = self.send_panel('files?action=GetTaskSpeed', {'limit': 10, 'type': 'all'})
        if not type(ret) == dict: return False
        if 'task' not in ret: return False
        if not ret['task']: return False
        if len(ret['task']) > 0:
            return ret['task']
        return False

    def dst_install_plugin(self, plugin_name, version, min_version=0):
        """
        远程服务器安装插件
        :param min_version:
        :param plugin_name: 宝塔的插件名称，如:php-5.2
        :param version: 插件版本，如：5.2
        :return:
        """
        ret = self.send_panel('plugin?action=install_plugin', {
            'sName': plugin_name,
            'version': version,
            'min_version': min_version,
            'type': 1
        })
        if not type(ret) == dict: return False
        if 'status' not in ret: return False
        if not ret['status']:
            helpr.write_log_error(f"远程服务器安装插件【{plugin_name}】失败:{ret['msg']}")
            return False
        return True


    def get_php_config(self, version: str):
        """
        获取远程宝塔中的PHP配置
        :param version: PHP版本，如74
        :return:
        """
        ret = self.send_panel('ajax?action=GetPHPConfig', {'version': version})
        if not type(ret) == dict: return False
        if 'disable_functions' not in ret:
            helpr.write_log_error(f"获取PHP配置失败:{ret['msg']}")
            return False
        return ret


    def install_php_ext(self, ext_name: str, version: str):
        """
        安装远程服务器的PHP扩展
        :param ext_name: 扩展名称，如：swoole
        :param version: PHP版本，如：74
        :return:
        """
        ret = self.send_panel('files?action=InstallSoft', {
            'name': ext_name,
            'version': version,
            'type': 1
        })
        if not type(ret) == dict: return False
        if 'status' not in ret: return False
        if not ret['status']:
            helpr.write_log_error(f"安装PHP扩展失败:{ret['msg']}")
        return True


    def set_php_disable_functions(self, php_version: str, disable_functions: str):
        """
        设置远程服务器的PHP禁用函数
        :param php_version: PHP版本，如74
        :param disable_functions: 禁用函数，如：shell_exec,passthru
        """
        ret = self.send_panel('config?action=setPHPDisable', {
            'version': php_version,
            'disable_functions': disable_functions
        })
        if not type(ret) == dict: return False
        if 'status' not in ret: return False
        if not ret['status']:
            helpr.write_log_error(f"设置PHP禁用函数失败:{ret['msg']}")
            return False
        return True
